﻿using System;

using Storm.TextEditor.Editor.Text;
using Storm.TextEditor.Editor.Undo;
using Storm.TextEditor.Parser.Objects;

namespace Storm.TextEditor.Editor.Interacting
{
    /// <summary>
    /// Represents the manager of user selection in a TextEditor.
    /// </summary>
    public class Selection
    {
        #region Fields

        private TextRange bounds = null;
        private TextEditorBase control = null;

        #region Events

        /// <summary>
        /// Occurs when the selection has changed.
        /// </summary>
        public event EventHandler Change = null;

        #endregion

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the currently selected text. If some text is already selected, overwrites that text.
        /// </summary>
        public String Text
        {
            get
            {
                if (this.IsValid == false)
                    return "";
                else
                    return control.Document.GetRange(this.LogicalBounds);
            }
            set
            {
                if (this.Text == value) return;

                // Bug fix.
                string tmp = value.Replace(Environment.NewLine, "\n");
                tmp = tmp.Replace("\n", Environment.NewLine);
                value = tmp;

                TextPoint caretPos = control.Caret.Position;
                int caretX = caretPos.X;
                int caretY = caretPos.Y;

                control.Document.StartUndoCapture();
                this.DeleteSelection();

                control.Document.InsertText(value, caretPos.X, caretPos.Y);
                this.SelLength = value.Length;

                if (caretX != caretPos.X || caretY != caretPos.Y)
                    control.Caret.Position = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);

                control.Document.EndUndoCapture();
                control.Document.InvokeChange();
            }
        }

        /// <summary>
        /// Gets the normalized positions of the selection.
        /// Swapping start and end values if the selection is reversed.
        /// </summary>
        public TextRange LogicalBounds
        {
            get
            {
                TextRange r = new TextRange();
                if (this.Bounds.FirstRow < this.Bounds.LastRow)
                    return Bounds;
                else if (this.Bounds.FirstRow == this.Bounds.LastRow &&
                    this.Bounds.FirstColumn < this.Bounds.LastColumn)
                {
                    return Bounds;
                }
                else
                {
                    r.FirstColumn = this.Bounds.LastColumn;
                    r.FirstRow = this.Bounds.LastRow;

                    r.LastColumn = this.Bounds.FirstColumn;
                    r.LastRow = this.Bounds.FirstRow;

                    return r;
                }
            }
        }

        /// <summary>
        /// Gets whether the current selection contains one or more characters.
        /// </summary>
        public bool IsValid
        {
            get
            {
                return (this.LogicalBounds.FirstColumn != this.LogicalBounds.LastColumn ||
                    this.LogicalBounds.FirstRow != this.LogicalBounds.LastRow);
            }
        }

        /// <summary>
        /// Gets or sets the length of the selection.
        /// </summary>
        public int SelLength
        {
            get
            {
                TextPoint p1 = new TextPoint(this.Bounds.FirstColumn, this.Bounds.FirstRow);
                TextPoint p2 = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);

                int i1 = control.Document.PointToIntPos(p1);
                int i2 = control.Document.PointToIntPos(p2);

                return i2 - i1;
            }
            set { this.SelEnd = this.SelStart + value; }
        }

        /// <summary>
        /// Gets or sets the current selection end index. (SelStart + SelLength basically)
        /// </summary>
        public int SelEnd
        {
            get
            {
                TextPoint p = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);
                return control.Document.PointToIntPos(p);
            }
            set
            {
                TextPoint p = control.Document.IntPosToPoint(value);

                this.Bounds.LastColumn = p.X;
                this.Bounds.LastRow = p.Y;
            }
        }


        /// <summary>
        /// Gets or sets the current selection start index.
        /// </summary>
        public int SelStart
        {
            get
            {
                TextPoint p = new TextPoint(this.Bounds.FirstColumn, this.Bounds.FirstRow);
                return control.Document.PointToIntPos(p);
            }
            set
            {
                TextPoint p = control.Document.IntPosToPoint(value);

                this.Bounds.FirstColumn = p.X;
                this.Bounds.FirstRow = p.Y;
            }
        }

        /// <summary>
        /// Gets or sets the logical selection start index.
        /// </summary>
        public int LogicalSelStart
        {
            get
            {
                TextPoint p = new TextPoint(this.LogicalBounds.FirstColumn, this.LogicalBounds.FirstRow);
                return control.Document.PointToIntPos(p);
            }
            set
            {
                TextPoint p = control.Document.IntPosToPoint(value);

                this.Bounds.FirstColumn = p.X;
                this.Bounds.FirstRow = p.Y;
            }
        }

        /// <summary>
        /// Gets or sets the selection bounds as a TextRange. 
        /// </summary>
        public TextRange Bounds
        {
            get { return bounds; }
            set
            {
                if (bounds != null)
                    bounds.Change -= new EventHandler(OnBoundsChange);

                bounds = value;
                bounds.Change += new EventHandler(OnBoundsChange);
                this.OnChange();
            }
        }

        #endregion

        #region Methods

        #region Public

        /// <summary>
        /// Increases the level of indentation at the current selection by one.
        /// </summary>
        public void Indent()
        {
            if (this.IsValid == false)
                return;

            Row row = null;
            UndoBlockCollection actionGroup = new UndoBlockCollection();
            for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
            {
                row = control.Document[i];

                UndoBlock undoBlock = new UndoBlock();
                undoBlock.Action = UndoAction.InsertRange;

                if (control.KeepTabs == true)
                {
                    row.Text = "\t" + row.Text;
                    undoBlock.Text = "\t";
                }
                else
                {
                    string indent = new string(' ', control.TabSpaces);
                    row.Text = indent + row.Text;
                    undoBlock.Text = indent;
                }

                undoBlock.Position.X = 0;
                undoBlock.Position.Y = i;

                actionGroup.Add(undoBlock);
            }

            if (actionGroup.Count > 0)
                control.Document.AddToUndoList(actionGroup);

            this.Bounds = this.LogicalBounds;
            this.Bounds.FirstColumn = 0;
            this.Bounds.LastColumn = row.Text.Length;

            control.Caret.Position.X = this.LogicalBounds.LastColumn;
            control.Caret.Position.Y = this.LogicalBounds.LastRow;
        }

        /// <summary>
        /// Decreases the level of indentation at the current selection by one.
        /// </summary>
        public void Outdent()
        {
            if (this.IsValid == false)
                return;

            Row row = null;
            UndoBlockCollection actionGroup = new UndoBlockCollection();
            for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
            {
                row = control.Document[i];

                UndoBlock undoBlock = new UndoBlock();
                undoBlock.Action = UndoAction.DeleteRange;

                undoBlock.Position.X = 0;
                undoBlock.Position.Y = i;

                actionGroup.Add(undoBlock);

                string s = row.Text;
                if (s.StartsWith("\t") == true)
                {
                    undoBlock.Text = s.Substring(0, 1);
                    s = s.Substring(1);
                }

                if (s.StartsWith(new string(' ', control.TabSpaces)) == true)
                {
                    undoBlock.Text = s.Substring(0, control.TabSpaces);
                    s = s.Substring(control.TabSpaces);
                }

                row.Text = s;
            }

            if (actionGroup.Count > 0)
                control.Document.AddToUndoList(actionGroup);

            this.Bounds = this.LogicalBounds;
            this.Bounds.FirstColumn = 0;
            this.Bounds.LastColumn = row.Text.Length;

            control.Caret.Position.X = this.LogicalBounds.LastColumn;
            control.Caret.Position.Y = this.LogicalBounds.LastRow;
        }

        /// <summary>
        /// Adds a string to the current selection.
        /// </summary>
        /// <param name="pattern">The string that will be added to the selection.</param>
        public void Indent(string pattern)
        {
            if (this.IsValid == false)
                return;

            Row row = null;
            UndoBlockCollection actionGroup = new UndoBlockCollection();
            for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
            {
                row = control.Document[i];
                row.Text = pattern + row.Text;

                UndoBlock undoBlock = new UndoBlock();
                undoBlock.Action = UndoAction.InsertRange;
                undoBlock.Text = pattern;

                undoBlock.Position.X = 0;
                undoBlock.Position.Y = i;

                actionGroup.Add(undoBlock);
            }

            if (actionGroup.Count > 0)
                control.Document.AddToUndoList(actionGroup);

            this.Bounds = this.LogicalBounds;
            this.Bounds.FirstColumn = 0;
            this.Bounds.LastColumn = row.Text.Length;

            control.Caret.Position.X = this.LogicalBounds.LastColumn;
            control.Caret.Position.Y = this.LogicalBounds.LastRow;
        }

        /// <summary>
        /// Adds a string to the current selection.
        /// </summary>
        /// <param name="pattern">The string that will be added to the selection.</param>
        public void Outdent(string pattern)
        {
            if (this.IsValid == false)
                return;

            Row row = null;
            UndoBlockCollection actionGroup = new UndoBlockCollection();
            for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
            {
                row = control.Document[i];

                UndoBlock undoBlock = new UndoBlock();
                undoBlock.Action = UndoAction.DeleteRange;
                undoBlock.Position.X = 0;
                undoBlock.Position.Y = i;

                actionGroup.Add(undoBlock);

                string s = row.Text;
                if (s.StartsWith(pattern) == true)
                {
                    undoBlock.Text = s.Substring(0, pattern.Length);
                    s = s.Substring(pattern.Length);
                }

                row.Text = s;
            }

            if (actionGroup.Count > 0)
                control.Document.AddToUndoList(actionGroup);

            this.Bounds = this.LogicalBounds;
            this.Bounds.FirstColumn = 0;
            this.Bounds.LastColumn = row.Text.Length;

            control.Caret.Position.X = this.LogicalBounds.LastColumn;
            control.Caret.Position.Y = this.LogicalBounds.LastRow;
        }

        /// <summary>
        /// Delete the active selection.
        /// <seealso cref="ClearSelection"/>
        /// </summary>
        public void DeleteSelection()
        {
            TextRange r = this.LogicalBounds;

            int x = r.FirstColumn;
            int y = r.FirstRow;

            control.Document.DeleteRange(r);
            control.Caret.Position.X = x;
            control.Caret.Position.Y = y;

            this.ClearSelection();
            control.ScrollIntoView();
        }

        /// <summary>
        /// Clear the active selection.
        /// <seealso cref="DeleteSelection"/>
        /// </summary>
        public void ClearSelection()
        {
            if (control != null)
            {
                this.Bounds.FirstColumn = control.Caret.Position.X;
                this.Bounds.FirstRow = control.Caret.Position.Y;

                this.Bounds.LastColumn = control.Caret.Position.X;
                this.Bounds.LastRow = control.Caret.Position.Y;
            }
        }

        /// <summary>
        /// Sets the bounds of the selection to the Caret start and end positions.
        /// </summary>
        public void MakeSelection()
        {
            this.Bounds.LastColumn = control.Caret.Position.X;
            this.Bounds.LastRow = control.Caret.Position.Y;
        }

        /// <summary>
        /// Selects all text in the TextEditor.
        /// </summary>
        public void SelectAll()
        {
            this.Bounds.FirstColumn = 0;
            this.Bounds.FirstRow = 0;

            this.Bounds.LastColumn = control.Document[control.Document.Count - 1].Text.Length;
            this.Bounds.LastRow = control.Document.Count - 1;

            control.Caret.Position.X = this.Bounds.LastColumn;
            control.Caret.Position.Y = this.Bounds.LastRow;
            control.ScrollIntoView();
        }

        #endregion

        #region EventHandlers

        private void OnBoundsChange(object s, EventArgs e)
        {
            this.OnChange();
        }

        private void OnPositionChange(object s, EventArgs e)
        {
            this.OnChange();
        }

        /// <summary>
        /// Raises the Change event.
        /// </summary>
        private void OnChange()
        {
            if (this.Change != null)
                this.Change(this, null);
        }

        #endregion

        #endregion

        /// <summary>
        /// Initializes a new instance of Selection.
        /// </summary>
        public Selection(TextEditorBase control)
        {
            this.control = control;
            this.Bounds = new TextRange();
        }
    }
}
